package main

/*
#cgo LDFLAGS: -L../lib -ljudge -O2 -Wl,-rpath-link=../lib
#include <stdlib.h>
#define ENABLE_DB_JUDGE
#include "../include/judge.h"
*/
import "C"

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"path/filepath"
	"log"
	"os"
	"os/exec"
	"time"
	"unsafe"
	"strconv"
	"strings"

	"redisClient"

	"github.com/Unknwon/goconfig"
)

type BackupCodeInfo struct {
	Id          string 		`json:"id"`
	Code        string 		`json:"code"`
	Language    string 		`json:"language"`
	JudgeType   int    		`json:"judge_type"`
	ProblemId   string 		`json:"problem_id"`
	Stdin       []string	`json:"stdin"` 
	Stdout      []string	`json:"stdout"` 
	InitRefId	string		`json:"init_ref_id"`
	DataRefId   string 		`json:"data_ref_id"`
	SpjRefId    string 		`json:"spj_ref_id"`
	Time        int    		`json:"time"`
	Memory      int64  		`json:"memory"`
	Compare     int    		`json:"compare"`
	Eps         string 		`json:"eps"`
	Ruler       int    		`json:"ruler"`
	QuerySet	int	   		`json:"query_set"`
	ResponseUrl string 		`json:"response_url"`
}

var dataPath string
var logPath string
var cachePath string
var userPath string
var tempDataPath string
var myLogFile string
var myConfFile string
var dockerId string
var redisSetPrefix string
var conf *goconfig.ConfigFile

func anyToJsonStr(object interface{}) (string, error) {
	object_json, err := json.Marshal(object)
	return string(object_json), err
}

func WriteStringToFile(filename, text string) string {
	f, err := os.OpenFile(filename, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644)
	if err != nil {
		log.Printf("open file error:%v", err)
		return "{\"code\":101,msg:\"打开文件失败\"}"
	}
	defer f.Close()
	n, err := f.WriteString(text)
	if err != nil {
		log.Printf("write file error:%v", err)
		return "{\"code\":102,msg:\"写入文件失败\"}"
	}
	if n != len(text) {
		log.Printf("write size file error:%v", err)
		return "{\"code\":103,msg:\"写入文件缺少字符\"}"
	}
	log.Printf("file:%s,size:%d\n", filename, n)
	return ""
}

func GetDockerId() string {
	//head -1 /proc/self/cgroup|cut -d/ -f3|cut -c1-12
	command := exec.Command("bash", "-c", "head -1 /proc/self/cgroup|cut -d/ -f3|cut -c1-12")
	command.Stdout = &bytes.Buffer{}
	command.Stderr = &bytes.Buffer{}
	err := command.Run()
	if err != nil {
		log.Printf("get container ID error:%v", err)
		return strconv.Itoa(os.Getpid())
	}
	temp_id := command.Stdout.(*bytes.Buffer).String()
	temp_id = strings.TrimSpace(temp_id)
	if temp_id == "" {
		return strconv.Itoa(os.Getpid())
	}
	return temp_id
}

func judgeByCodeInfo(codeInfo BackupCodeInfo, nowLogPath string) string {
	recv_json, _ := anyToJsonStr(codeInfo)
	log.Printf("recv task:%s", recv_json)
	filename, err := conf.GetValue("FILENAME", codeInfo.Language)
	if err != nil || filename == "" {
		return `{"code":100,"msg":"当前语言类型不支持","error":"当前语言类型不支持"}`
	} else {
		log.Printf("%s->%s\n", codeInfo.Language, filename)
	}
	filename = userPath + "/" + filename

	log.Printf("write file:%s\n", filename)
	log.Printf("problemId : %s", codeInfo.ProblemId)

	var flag string
	flag = WriteStringToFile(filename, codeInfo.Code)
	if flag != "" {
		return flag
	}

	nowSpjCachePath := "" // 空串表示使用临时文件夹
	nowUserCachePath := "" // 空串表示使用临时文件夹

	nowIintPath := dataPath + "/" + codeInfo.InitRefId // 初始化目录
	nowDataPath := "" // 题目文件，如果传入的codeInfo.Stdin不为空则将把codeInfo.Stdin写入到一个临时文件夹，同时将此文件夹作为nowDataPath
	nowSpjPath := "" // 空串表示回去dataPath下寻找spj文件

	if codeInfo.ProblemId != "" {
		nowUserCachePath = cachePath + "/" + codeInfo.ProblemId + "/user"
		nowSpjCachePath = cachePath + "/" + codeInfo.ProblemId + "/spj"
	}

	if codeInfo.SpjRefId != "" {
		nowSpjPath = dataPath + "/" + codeInfo.SpjRefId
	}

	if codeInfo.DataRefId != "" {
		nowDataPath = dataPath + "/" + codeInfo.DataRefId
	}

	// 若存在stdin则替换nowDataPath
	if len(codeInfo.Stdin) != 0 {
		// 删除临时题库中的所有文件
		files, err := filepath.Glob(tempDataPath + "/*")
		if err != nil {
			log.Printf("find datafile error:%v", err)
			return `{"code":106,"msg":"查询文件出现错误","error":"查询历史文件出错"}`
		}
		for _, f := range files {
			if err := os.Remove(f); err != nil {
				log.Printf("rm datafile error:%v", err)
				return `{"code":107,"msg":"清理文件出现错误","error":"删除历史文件出错"}`
			}
		}

		log.Printf("write user example\nstdin Value:%v", codeInfo.Stdin)
		for i := 0; i < len(codeInfo.Stdin); i++ {
			stdin := codeInfo.Stdin[i]
			flag = WriteStringToFile(tempDataPath + fmt.Sprintf("/webData%d.in", i), stdin)
			if flag != "" {
				return flag
			}
		}
		log.Printf("write user example\nstdout Value:%v", codeInfo.Stdout)
		for i := 0; i < len(codeInfo.Stdout); i++ {
			stdout := codeInfo.Stdout[i]
			flag = WriteStringToFile(tempDataPath + fmt.Sprintf("/webData%d.out", i), stdout)
			if flag != "" {
				return flag
			}
		}
		nowDataPath = tempDataPath
	}

	// 写入源码日志
	flag = WriteStringToFile(nowLogPath+"/src.log", codeInfo.Code)
	if flag != "" {
		return flag
	}

	// 写入参数信息
	flag = WriteStringToFile(nowLogPath+"/options.log",
		fmt.Sprintf("lang:%s\n"+
			"judgeType:%d\n"+
			"filename:%s\n"+
			"initPath:%s\n"+
			"dataPath:%s\n"+
			"timeLimit:%d\n"+
			"memoryLimit:%d\n"+
			"compareFlag:%d\n"+
			"ruler:%d\n"+
			"query_set:%d\n"+
			"eps:%s\n"+
			"userPath:%s\n"+
			"spjPath:%s\n"+
			"spjRefPath:%s\n",
			codeInfo.Language,
			codeInfo.JudgeType,
			filename,
			nowIintPath,
			nowDataPath,
			nowSpjPath,
			codeInfo.Time,
			codeInfo.Memory,
			codeInfo.Compare,
			codeInfo.Ruler,
			codeInfo.QuerySet,
			codeInfo.Eps,
			nowUserCachePath,
			nowSpjCachePath,
		))

	C.SetOutputFile(C.CString(nowLogPath + "/output.log"))
	C.SetErrorFile(C.CString(nowLogPath + "/error.log"))
	res := C.RunJudge(
		C.CString(codeInfo.Language),
		C.int(codeInfo.JudgeType),
		C.CString(filename),
		C.CString(nowIintPath),
		C.CString(nowDataPath),
		C.CString(nowSpjPath),
		C.int(codeInfo.Time),
		C.int(codeInfo.Memory),
		C.int(codeInfo.Compare),
		C.int(codeInfo.Ruler),
		C.int(codeInfo.QuerySet),
		C.CString(codeInfo.Eps),
		C.CString(nowUserCachePath),
		C.CString(nowSpjCachePath),
	)
	defer C.free(unsafe.Pointer(res))

	// 写入结果日志
	flag = WriteStringToFile(nowLogPath+"/result.log", C.GoString(res))

	return C.GoString(res)
}

func main() {
	var err error
	if len(os.Args) < 2 {
		fmt.Printf("args count must be equal 1")
		return
	}

	myConfFile = os.Args[1]

	conf, err = goconfig.LoadConfigFile(myConfFile)
	if err != nil {
		log.Fatal("ReadConfigure:", err.Error())
	}

	myLogFile, err = conf.GetValue("BACKUP_JUDGE", "BACKUP_JUDGE_LOGFILE")
	if err != nil || myLogFile == "" {
		log.Fatal("ReadConfigure BACKUP_JUDGE_LOGFILE error or param is null:", err)
	}

	dataPath, err = conf.GetValue("BACKUP_JUDGE", "BACKUP_JUDGE_PROBLEM_FILE_PATH")
	if err != nil || dataPath == "" {
		log.Fatal("ReadConfigure BACKUP_JUDGE_PROBLEM_FILE_PATH error or param is null:", err)
	}

	logPath, err = conf.GetValue("BACKUP_JUDGE", "BACKUP_JUDGE_LOGFILE_PATH")
	if err != nil || logPath == "" {
		log.Fatal("ReadConfigure BACKUP_JUDGE_LOGFILE_PATH error or param is null:", err)
	}

	cachePath, err = conf.GetValue("BACKUP_JUDGE", "BACKUP_JUDGE_CACHE_PATH")
	if err != nil || cachePath == "" {
		log.Fatal("ReadConfigure BACKUP_JUDGE_SPJ_CODE_PATH error or param is null:", err)
	}

	userPath, err = conf.GetValue("BACKUP_JUDGE", "BACKUP_JUDGE_USER_CODE_PATH")
	if err != nil || userPath == "" {
		log.Fatal("ReadConfigure BACKUP_JUDGE_USER_CODE_PATH error or param is null:", err)
	}

	tempDataPath, err = conf.GetValue("BACKUP_JUDGE", "BACKUP_JUDGE_TEMP_DATA_PATH")
	if err != nil || tempDataPath == "" {
		log.Fatal("ReadConfigure BACKUP_JUDGE_TEMP_DATA_PATH error or param is null:", err)
	}

	err = os.MkdirAll(cachePath, 0755)
	err = os.MkdirAll(userPath, 0755)
	err = os.MkdirAll(tempDataPath, 0755)

	dockerId = GetDockerId()
	consumer := dockerId
	fmt.Printf("consumer : %s\n", consumer)

	logPath = logPath + "/backup_judge_" + consumer[0:6]
	err = os.MkdirAll(logPath, 0755)
	if err != nil && !os.IsExist(err) {
		log.Fatalf("mkdir error:%v", err)
	}
	logFile, err := os.OpenFile(logPath+"/"+myLogFile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0755)
	if err != nil && os.IsExist(err) {
		panic(err)
	}
	log.SetOutput(logFile)

	conf, err = goconfig.LoadConfigFile(myConfFile)
	if err != nil {
		log.Fatal("ReadConfigure:", err.Error())
	}

	log.SetOutput(logFile)
	host, _ := conf.GetValue("REDIS", "REDIS_HOST")
	port, _ := conf.GetValue("REDIS", "REDIS_PORT")
	pwd, _ := conf.GetValue("REDIS", "REDIS_PWD")
	db, _ := conf.Int("REDIS", "REDIS_DATABASE")
	redisSetPrefix, _ = conf.GetValue("REDIS", "REDIS_SET_PREFIX")
	stream, err := conf.GetValue("REDIS", "REDIS_STREAM_CODE")
	if err != nil {
		log.Fatal("ReadConfigure STREAM:", err)
	}
	group, err := conf.GetValue("REDIS", "REDIS_GROUP_CODE")
	if err != nil {
		log.Fatal("ReadConfigure GROUP:", err)
	}

	// nextQueue将会以url-{response_url}的形式作为stream名
	// nextQueue, _ := conf.GetValue("REDIS", "REDIS_STREAM_RESPONSE")
	// fmt.Println("judging")
	var ResultGroup string
	ResultGroup, err = conf.GetValue("REDIS", "REDIS_GROUP_RESPONSE")
	if err != nil {
		log.Fatal("ReadConfigure REDIS_GROUP_RESPONSE:", err)
	}
	redisClient.InitClient(host, port, pwd, db)
	redisClient.RedisCreateGroup(stream, group, "0")

	ERROR_REPORT_JSON := `{"result" : "SYS_ERROR"}`

	res := C.InitDatabaseParamsFromFile(C.CString(myConfFile))
	if int(res) == 0 {
		log.Fatal("database judge init parmas error")
	}

	// 每次我们只会进行一次代码的运行，所以只拿一个就是全部未ack的Message
	pendingMsgs := redisClient.RedisXPending(stream, group, "-", "+", 1, consumer)

	// 记录是否有新加入的url地址,如果有则创建新的消费者组及其对应流
	urlVisited := map[string]bool{}

	if len(pendingMsgs) != 0 {
		pendingMsg := pendingMsgs[0]
		key := pendingMsg.Id
		checkMsgs := redisClient.RedisXRange(stream, key, key, 1)
		var result string
		var tempCodeInfo BackupCodeInfo

		if len(checkMsgs) != 0 {
			checkMsg := checkMsgs[0]
			if err = json.Unmarshal([]byte(checkMsg.Values["codeInfo"].(string)), &tempCodeInfo); err != nil {
				temp, _ := json.Marshal(map[string]interface{}{
					"code":  77,
					"msg":   "fail unmarshal post data",
					"error": fmt.Sprintf("%v", err),
				})
				result = string(temp)
			} else {
				dirname := key
				dirname = logPath + "/" + dirname
				content, err := ioutil.ReadFile(dirname + "/result.log")
				if err != nil || len(content) == 0 {
					log.Printf("restart check, code not run, id:%s", key)
					result = fmt.Sprintf(`"code":0, "msg":"server restart check message error", "data" : %s`, ERROR_REPORT_JSON)
				} else {
					result = string(content)
				}
			}

			nextQueue := "url-" + tempCodeInfo.ResponseUrl
			// 若当前stream没有被记录过,则新建一个消费者组同时新建流
			if _, ok := urlVisited[nextQueue]; !ok {
				redisClient.RedisCreateGroup(nextQueue, ResultGroup, "0")
				urlVisited[nextQueue] = true
			}
			id := redisClient.RedisAddStream(nextQueue, map[string]interface{}{
				"result": result,
				"id":     tempCodeInfo.Id,
			})
			if id != "" {
				log.Printf("task result add next queue,id:%s\n", key)
			} else {
				log.Printf("task result failed to add next queue,id:%s\n", key)
			}
			ok := redisClient.RedisAckGroup(stream, group, key)
			if ok > 0 {
				log.Printf("task ack done,id:%s\n", key)
			} else {
				log.Printf("task ack fail,id:%s\n", key)
			}
		} else {
			log.Printf("message loss, id:%s", key)
		}
	}

	for {
		task := redisClient.RedisReadGroup(
			[]string{stream, ">"},
			group,
			consumer,
			1,
			0*time.Second,
		)
		if len(task) == 0 {
			// fmt.Println("no task")
			continue
		}
		key := task[0].Messages[0].ID
		// 创建日志目录
		dirname := key
		dirname = logPath + "/" + dirname
		err := os.MkdirAll(dirname, 0755)
		if err != nil && !os.IsExist(err) {
			log.Fatalf("mkdir error:%v", err)
		}

		logFile.Close()
		logFile, err = os.OpenFile(dirname+"/"+myLogFile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0755)
		if err != nil && os.IsExist(err) {
			panic(err)
		}
		log.SetOutput(logFile)
		log.Printf("get task,id:%s\n", key)

		codeInfoStr := task[0].Messages[0].Values["codeInfo"].(string)
		codeInfo := new(BackupCodeInfo)
		var result string
		if err = json.Unmarshal([]byte(codeInfoStr), &codeInfo); err != nil {
			temp, _ := json.Marshal(map[string]interface{}{
				"code":  77,
				"msg":   "fail unmarshal post data",
				"error": fmt.Sprintf("%v", err),
			})
			result = string(temp)
		} else {
			result = judgeByCodeInfo(*codeInfo, dirname)
		}
		log.Println("result:", result)

		// 无需上报的用户自测样例
		if codeInfo.ResponseUrl == "" {
			setKey := redisSetPrefix + key
			setok := redisClient.RedisSet(setKey, result, time.Minute*5)
			if setok {
				log.Printf("task result set done,id:%s\n", key)
			} else {
				log.Printf("task result set fail,id:%s\n", key)
			}
			ok := redisClient.RedisAckGroup(stream, group, key)
			if ok > 0 {
				log.Printf("task ack done,id:%s\n", key)
			} else {
				log.Printf("task ack fail,id:%s\n", key)
			}

		} else {
			nextQueue := "url-" + codeInfo.ResponseUrl
			// 若当前stream没有被记录过,则新建一个消费者组同时新建流
			if _, ok := urlVisited[nextQueue]; !ok {
				redisClient.RedisCreateGroup(nextQueue, ResultGroup, "0")
				urlVisited[nextQueue] = true
			}
			id := redisClient.RedisAddStream(nextQueue, map[string]interface{}{
				"result": result,
				"id":     codeInfo.Id,
			})
			if id != "" {
				log.Printf("task result add next queue,id:%s\n", key)
			} else {
				log.Printf("task result failed to add next queue,id:%s\n", key)
			}
			ok := redisClient.RedisAckGroup(stream, group, key)
			if ok > 0 {
				log.Printf("task ack done,id:%s\n", key)
			} else {
				log.Printf("task ack fail,id:%s\n", key)
			}
		}
	}
}
			